import json, os, sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QGridLayout, QMessageBox, QSlider, QCheckBox
from PyQt5.QtGui import QFont, QIcon, QPixmap, QKeyEvent, QCloseEvent
from PyQt5.QtCore import Qt, QSize, QUrl
from PyQt5.QtMultimedia import QMediaContent, QMediaPlayer


class ConfigWindow(QWidget):
    """一个模态的窗口,用于设置音量调节、是否置顶、五八三五模式等"""
    def __init__(self, parent, volume: int):
        super(ConfigWindow, self).__init__()
        self.parent = parent
        self.volume = volume
        self.top = False
        self.idiot = False
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle("设置")
        self.setWindowIcon(QIcon(r'res/images/icon.png'))
        self.setFixedSize(300, 150)
        self.setWindowModality(Qt.WindowModality.ApplicationModal)

        self.label_volume = QLabel("音量")
        self.volume_slider = QSlider(Qt.Orientation.Horizontal)
        self.volume_slider.setRange(0, 100)
        self.volume_slider.setValue(self.volume)
        self.volume_slider.setFixedWidth(150)
        self.volume_slider.setFixedHeight(20)
        self.volume_slider.setStyleSheet("""
            QSlider::groove:horizontal {
                border: none;
            }
            QSlider::sub-page:horizontal {/*左滑槽*/
                background: #000;
                border-radius: 4px;
                margin-top: 6px;
                margin-bottom: 6px;
            }
            QSlider::add-page:horizontal {/*右滑槽*/
                background: #aaa;
                border-radius: 4px;
                margin-top: 6px;
                margin-bottom: 6px;
            }
            QSlider::handle:horizontal {/*滑块*/
                background: #444;
                width: 16px;
                border-radius: 8px;
                margin-top: 2px;
                margin-bottom: 2px;
            }
            QSlider::handle:horizontal:hover {/*滑块悬停*/
                background: #333;
                width: 20px;
                border-radius: 10px;
                margin-top: 0px;
                margin-bottom: 0px;
            }
        """)

        self.label_top = QLabel("置顶")
        self.top_checkbox = QCheckBox()
        self.top_checkbox.setChecked(self.top)

        self.label_idiot = QLabel("5835")
        self.idiot_checkbox = QCheckBox()
        self.idiot_checkbox.setChecked(self.idiot)

        layout = QGridLayout()
        layout.addWidget(self.label_volume, 0, 0, 1, 2)
        layout.addWidget(self.volume_slider, 0, 2, 1, 4)
        layout.addWidget(self.top_checkbox, 1, 0, 1, 1)
        layout.addWidget(self.label_top, 1, 1, 1, 2)
        layout.addWidget(self.idiot_checkbox, 1, 3, 1, 1)
        layout.addWidget(self.label_idiot, 1, 4, 1, 2)
        self.setLayout(layout)
    
    def keyPressEvent(self, a0: QKeyEvent):
        if a0.key() == Qt.Key.Key_Escape:
            self.close()
    
    def closeEvent(self, a0: QCloseEvent):
        self.volume = self.volume_slider.value()
        self.top = self.top_checkbox.isChecked()
        self.idiot = self.idiot_checkbox.isChecked()
        self.parent.set_volume(self.volume)
        self.parent.set_top(self.top)
        self.parent.set_idiot(self.idiot)
    
    def show(self):  # 主窗口置顶的话它弹出的时候也得置顶，不然会被挡住
        self.setWindowFlag(Qt.WindowStaysOnTopHint, self.top)
        super().show()


class MediaPlayerGroup:
    """
    QMediaPlayer似乎是抢占式的,为了让音频能在一定程度上被同时播放,在这里同时管理多个QMediaPlayer。
    但要注意这种做法只适合较短的音效,如果希望用QMediaPlayer播放BGM,请自己创建一个QMediaPlayer并管理。
    """
    def __init__(self, parent=None, player_num: int=3):
        self.player_num = player_num
        self.players = [QMediaPlayer(parent) for _ in range(self.player_num)]
        self.cur_idx = 0

    def play(self, content: QMediaContent):
        idx = self.cur_idx
        self.cur_idx = (self.cur_idx + 1) % self.player_num
        self.players[idx].setMedia(content)
        self.players[idx].play()

    def setVolume(self, volume: int):
        for player in self.players:
            player.setVolume(volume)


class Calculator(QWidget):
    operators = {'+', '-', '×', '÷'}

    def __init__(self, parent=None, version: str='', fullname: str='', nickname: str='', volume: int=100):
        super(Calculator, self).__init__()
        self.version = version
        self.fullname = fullname
        self.nickname = nickname
        self.volume = volume
        self.top = False
        self.idiot = False
        self.initUI()
        self.player = MediaPlayerGroup(self, 3)
        self.player.setVolume(self.volume)
        self.sounds = {'one': None, 'two': None, 'three': None, 'four': None, 'five': None, 'six': None, 'seven': None, 'eight': None, 'nine': None, 'zero': None, 'point': None, 'plus': None, 'minus': None, 'multiply': None, 'divide': None, 'equal': None, 'clear': None, 'delete': None, 'logo': None, 'error': None}
        self.configWindow = ConfigWindow(self, volume)
    
    def initUI(self):
        self.resize(450, 650)
        self.setFixedSize(self.width(), self.height())
        self.setWindowTitle(f'{self.fullname}计算器 {self.version}')
        self.setWindowIcon(QIcon(r'res/images/icon.png'))
        self.setWindowOpacity(0.95)
        
        self.result = QLabel('0', self)
        self.result.setFont(QFont('Microsoft YaHei', 50))
        self.result.setAlignment(Qt.AlignmentFlag.AlignRight)
        self.result.setFixedHeight(100)
        
        self.logo = QPushButton(self)
        self.logo.setFixedSize(100, 100)
        self.logo.setIcon(QIcon(r'res/images/logo.png'))
        self.logo.setIconSize(QSize(100, 100))
        self.logo.setFlat(True)
        self.logo.setStyleSheet('''QPushButton{border:none;background-color:transparent;}''')
        self.logo.clicked.connect(self.logo_clicked)
        self.clear = QPushButton('清空', self)
        self.clear.setFixedSize(100, 100)
        self.clear.clicked.connect(self.clear_clicked)
        self.delete = QPushButton('删除', self)
        self.delete.setFixedSize(100, 100)
        self.delete.clicked.connect(self.delete_clicked)
        self.divide = QPushButton('÷', self)
        self.divide.setFixedSize(100, 100)
        self.divide.clicked.connect(self.divide_clicked)
        self.seven = QPushButton('7', self)
        self.seven.setFixedSize(100, 100)
        self.seven.clicked.connect(self.seven_clicked)
        self.eight = QPushButton('8', self)
        self.eight.setFixedSize(100, 100)
        self.eight.clicked.connect(self.eight_clicked)
        self.nine = QPushButton('9', self)
        self.nine.setFixedSize(100, 100)
        self.nine.clicked.connect(self.nine_clicked)
        self.multiply = QPushButton('×', self)
        self.multiply.setFixedSize(100, 100)
        self.multiply.clicked.connect(self.multiply_clicked)
        self.four = QPushButton('4', self)
        self.four.setFixedSize(100, 100)
        self.four.clicked.connect(self.four_clicked)
        self.five = QPushButton('5', self)
        self.five.setFixedSize(100, 100)
        self.five.clicked.connect(self.five_clicked)
        self.six = QPushButton('6', self)
        self.six.setFixedSize(100, 100)
        self.six.clicked.connect(self.six_clicked)
        self.minus = QPushButton('-', self)
        self.minus.setFixedSize(100, 100)
        self.minus.clicked.connect(self.minus_clicked)
        self.one = QPushButton('1', self)
        self.one.setFixedSize(100, 100)
        self.one.clicked.connect(self.one_clicked)
        self.two = QPushButton('2', self)
        self.two.setFixedSize(100, 100)
        self.two.clicked.connect(self.two_clicked)
        self.three = QPushButton('3', self)
        self.three.setFixedSize(100, 100)
        self.three.clicked.connect(self.three_clicked)
        self.plus = QPushButton('+', self)
        self.plus.setFixedSize(100, 100)
        self.plus.clicked.connect(self.plus_clicked)
        self.zero = QPushButton('0', self)
        self.zero.setFixedHeight(100)
        self.zero.clicked.connect(self.zero_clicked)
        self.point = QPushButton('.', self)
        self.point.setFixedSize(100, 100)
        self.point.clicked.connect(self.point_clicked)
        self.equal = QPushButton('=', self)
        self.equal.setFixedSize(100, 100)
        self.equal.clicked.connect(self.equal_clicked)

        layout = QGridLayout()
        self.setLayout(layout)
        layout.addWidget(self.result, 0, 0, 1, 4)
        layout.addWidget(self.logo, 1, 0, 1, 1)
        layout.addWidget(self.clear, 1, 1, 1, 1)
        layout.addWidget(self.delete, 1, 2, 1, 1)
        layout.addWidget(self.divide, 1, 3, 1, 1)
        layout.addWidget(self.seven, 2, 0, 1, 1)
        layout.addWidget(self.eight, 2, 1, 1, 1)
        layout.addWidget(self.nine, 2, 2, 1, 1)
        layout.addWidget(self.multiply, 2, 3, 1, 1)
        layout.addWidget(self.four, 3, 0, 1, 1)
        layout.addWidget(self.five, 3, 1, 1, 1)
        layout.addWidget(self.six, 3, 2, 1, 1)
        layout.addWidget(self.minus, 3, 3, 1, 1)
        layout.addWidget(self.one, 4, 0, 1, 1)
        layout.addWidget(self.two, 4, 1, 1, 1)
        layout.addWidget(self.three, 4, 2, 1, 1)
        layout.addWidget(self.plus, 4, 3, 1, 1)
        layout.addWidget(self.zero, 5, 0, 1, 2)
        layout.addWidget(self.point, 5, 2, 1, 1)
        layout.addWidget(self.equal, 5, 3, 1, 1)
    
    def logo_clicked(self):
        self.configWindow.show()
        self.play_sound('logo')
    
    def clear_clicked(self):
        self.play_sound('clear')
        self.result.setText('0')
    
    def delete_clicked(self):
        self.play_sound('delete')
        if len(self.result.text()) > 1:
            self.result.setText(self.result.text()[:-1])
        else:
            self.result.setText('0')
    
    def divide_clicked(self):
        self.play_sound('divide')
        self.input('÷')
    
    def seven_clicked(self):
        self.play_sound('seven')
        self.input('7')

    def eight_clicked(self):
        self.play_sound('eight')
        self.input('8')

    def nine_clicked(self):
        self.play_sound('nine')
        self.input('9')

    def multiply_clicked(self):
        self.play_sound('multiply')
        self.input('×')

    def four_clicked(self):
        self.play_sound('four')
        self.input('4')

    def five_clicked(self):
        self.play_sound('five')
        self.input('5')

    def six_clicked(self):
        self.play_sound('six')
        self.input('6')

    def minus_clicked(self):
        self.play_sound('minus')
        self.input('-')

    def one_clicked(self):
        self.play_sound('one')
        self.input('1')

    def two_clicked(self):
        self.play_sound('two')
        self.input('2')

    def three_clicked(self):
        self.play_sound('three')
        self.input('3')

    def plus_clicked(self):
        self.play_sound('plus')
        self.input('+')

    def zero_clicked(self):
        self.play_sound('zero')
        self.input('0')

    def point_clicked(self):
        self.play_sound('point')
        self.input('.')

    def equal_clicked(self):
        self.play_sound('equal')
        if self.result.text()[-1] in Calculator.operators:
            self.result.setText(self.result.text()[:-1])
        self.calculate()
    
    def play_sound(self, name: str):
        if self.sounds[name] is None:  # lazy loading
            path = f'res/sound/{name}.mp3'
            if os.path.exists(path):
                self.sounds[name] = QMediaContent(QUrl.fromLocalFile(path))
            else:
                return
        self.player.play(self.sounds[name])
    
    def input(self, ch: str):
        if 48 <= ord(ch) <= 57:  # 数字
            if self.result.text() == '0':  # 显示屏为0,直接覆盖
                self.result.setText(ch)
            elif ch == '0':  # 输入0
                fin_op_pos = -1
                for i in range(len(self.result.text()) - 1, -1, -1):
                    if self.result.text()[i] in Calculator.operators:
                        fin_op_pos = i
                        break
                b = self.result.text()[fin_op_pos + 1:] if fin_op_pos != -1 else self.result.text()
                if '.' in b or b != '0':  # 如果有小数点或者不是0,则可以输入(即不允许012.3这样的情况)
                    self.result.setText(self.result.text() + ch)
            else:  # 输入其他数字
                fin_op_pos = -1
                for i in range(len(self.result.text()) - 1, -1, -1):
                    if self.result.text()[i] in Calculator.operators:
                        fin_op_pos = i
                        break
                b = self.result.text()[fin_op_pos + 1:] if fin_op_pos != -1 else self.result.text()
                if b == '0':  # 如果是0,覆盖
                    self.result.setText(self.result.text()[:-1] + ch)
                else:
                    self.result.setText(self.result.text() + ch)
        elif ch == '.':
            if 48 <= ord(self.result.text()[-1]) <= 57:  # 如果最后一位是数字
                fin_op_pos = 0
                for i in range(len(self.result.text()) - 1, -1, -1):
                    if self.result.text()[i] in Calculator.operators:
                        fin_op_pos = i
                        break
                if '.' not in self.result.text()[fin_op_pos:]:  # 如果没有小数点(既不允许多个小数点)
                    self.result.setText(self.result.text() + ch)
        else:  # 运算符
            if self.result.text()[-1] in Calculator.operators:  # 如果最后一位是运算符则覆盖
                self.result.setText(self.result.text()[:-1])
            self.calculate(ch)
    
    def calculate(self, next_operator: str=''):
        error, title, text = False, '', ''
        try:
            expression = self.result.text().replace('×', '*').replace('÷', '/')
            if self.idiot and expression in { '5*8', '5.0*8', '5*8.0', '5.0*8.0' }:
                self.result.setText('35')
            else:
                self.result.setText(str(eval(expression)))
            self.result.setText(self.result.text() + next_operator)
        except ZeroDivisionError:
            error = True
            title = f'来自{self.nickname}的提醒：'
            text = '0不可以作为除数哦！'
        except Exception as e:
            error = True
            title = f'{self.nickname}也不知道发生了什么，不过，好像很厉害？'
            text = f'{type(e).__name__}: {e}'
        if error:
            self.play_sound('error')
            msg = QMessageBox()
            msg.setWindowIcon(QIcon(r'res/images/icon.png'))
            msg.setIconPixmap(QPixmap(r'res/images/error.png').scaled(100, 100))
            msg.setWindowTitle(title)
            msg.setText(text)
            msg.exec_()
    
    def keyPressEvent(self, a0: QKeyEvent) -> None:
        if a0.key() == Qt.Key.Key_0:
            self.zero.click()
        elif a0.key() == Qt.Key.Key_1:
            self.one.click()
        elif a0.key() == Qt.Key.Key_2:
            self.two.click()
        elif a0.key() == Qt.Key.Key_3:
            self.three.click()
        elif a0.key() == Qt.Key.Key_4:
            self.four.click()
        elif a0.key() == Qt.Key.Key_5:
            self.five.click()
        elif a0.key() == Qt.Key.Key_6:
            self.six.click()
        elif a0.key() == Qt.Key.Key_7:
            self.seven.click()
        elif a0.key() == Qt.Key.Key_8:
            self.eight.click()
        elif a0.key() == Qt.Key.Key_9:
            self.nine.click()
        elif a0.key() == Qt.Key.Key_Plus:
            self.plus.click()
        elif a0.key() == Qt.Key.Key_Minus:
            self.minus.click()
        elif a0.key() == Qt.Key.Key_Asterisk:
            self.multiply.click()
        elif a0.key() == Qt.Key.Key_Slash:
            self.divide.click()
        elif a0.key() == Qt.Key.Key_Period:
            self.point.click()
        elif a0.key() == Qt.Key.Key_Equal or a0.key() == Qt.Key.Key_Enter or a0.key() == Qt.Key.Key_Return:
            self.equal.click()
        elif a0.key() == Qt.Key.Key_Backspace:
            self.delete.click()
        elif a0.key() == Qt.Key.Key_C:
            self.clear.click()
        elif a0.key() == Qt.Key.Key_Escape:
            if self.result.text() == '0':
                self.close()
            else:
                self.clear.click()

    def set_volume(self, volume: int) -> None:
        if self.volume == volume:
            return
        self.volume = volume
        self.player.setVolume(self.volume)
        try:
            with open(r'res/config.json', 'w', encoding='utf-8') as f:
                json.dump({'fullname': self.fullname, 'nickname': self.nickname, 'volume': self.volume}, f, ensure_ascii=False)
        except Exception as e:
            print(e)

    def set_top(self, top: bool) -> None:
        if self.top == top:
            return
        self.top = top
        self.setWindowFlag(Qt.WindowStaysOnTopHint, self.top)
        if not self.isVisible():  # 似乎设置置顶的时候会hide一下？反正不加这两行代码就会退出程序
            self.setVisible(True)

    def set_idiot(self, idiot: bool) -> None:
        if self.idiot == idiot:
            return
        self.idiot = idiot


if __name__ == '__main__':
    # cd到当前文件所在目录，以免从奇怪的地方打开导致程序中的相对位置失效
    os.chdir(os.path.split(os.path.realpath(__file__))[0])
    # 设置任务栏图标
    try:
        import ctypes
        ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('haruka-calculator')
    except Exception as e:  # 不支持就算了
        pass
    finally:
        del ctypes
    # 读取配置文件
    try:
        with open(r'res/config.json', 'r', encoding='utf-8') as f:
            config = json.load(f)
    except Exception as e:
        config = {}
    # 启动程序
    app = QApplication(sys.argv)
    app.setFont(QFont('Microsoft YaHei', 15))
    calculator = Calculator(version='v1.0', fullname=config.get('fullname', '白神遥'), nickname=config.get('nickname', '豹豹'), volume=config.get('volume', 100))
    calculator.show()
    sys.exit(app.exec_())
